﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;

namespace Plotter3DWinForms
{
    public partial class Plotter3D : UserControl
    {
        private Plot3d _plot3D;
        private Graphics _graphics;
        private Point3d _eye;
        private Point3d[] _axes = new Point3d[4];

        private float _scalW = 30;
        private float _scalH = 30;
        private float _tranW = 2;
        private float _tranH = 2;

        private int _addingLines = 30;

        private int _xLen = 100;
        private int _yLen = 100;

        private const int _offset = 30;

        public Plotter3D()
        {
            InitializeComponent();

            const int X = 40, Y = 20, Z = 20;

            _eye = new Point3d(X, Y, Z);

            _axes[0] = new Point3d(0, 0, 0);
            _axes[1] = new Point3d(_offset, 0, 0);
            _axes[2] = new Point3d(0, _offset, 0);
            _axes[3] = new Point3d(0, 0, _offset);
        }

        private void PlotData(Point3d[,] points, int xLen, int yLen)
        {
            double x1, y1, x2, y2;
            var myPen = new Pen(Color.Black, 0);

            for (int x = 0; x < xLen; ++x)
            {
                x2 = points[x, 0].TransCoord[0];
                y2 = points[x, 0].TransCoord[1];

                for (int y = 1; y < yLen; ++y)
                {
                    x1 = x2; y1 = y2;
                    x2 = points[x, y].TransCoord[0];
                    y2 = points[x, y].TransCoord[1];
                    _graphics.DrawLine(myPen, (float)x1, (float)y1, (float)x2, (float)y2);
                }

            }

            for (int y = 0; y < yLen; ++y)
            {
                x2 = points[0, y].TransCoord[0];
                y2 = points[0, y].TransCoord[1];

                for (int x = 1; x < xLen; ++x)
                {
                    x1 = x2; y1 = y2;
                    x2 = points[x, y].TransCoord[0];
                    y2 = points[x, y].TransCoord[1];
                    _graphics.DrawLine(myPen, (float)x1, (float)y1, (float)x2, (float)y2);
                }
            }
        }

        private void PrepareData(Point3d[,] points, int xLen, int yLen, Plot3d plot3D, Matrix3d transformMatrix,  out double ZMin, out double ZMax)
        {
            ZMax = Double.MinValue;
            ZMin = Double.MaxValue;

            for (int i = 0; i < xLen; ++i)
            {

                double x = (int)((plot3D.XMax - plot3D.XMin) / xLen + 1) * i + plot3D.XMin;

                for (int j = 0; j < yLen; ++j )
                {

                    double y = (int)((plot3D.YMax - plot3D.YMin) / yLen + 1) * j + plot3D.YMin;
                    double z = plot3D.Z(x, y);

                    if (z > ZMax)
                    {
                        ZMax = z;
                    }

                    if (z < ZMin)
                    {
                        ZMin = z;
                    } 

                    points[i, j] = new Point3d(x, y, z);
                    points[i, j].Transformation(transformMatrix);
                }

            }
        }

        public Matrix3d CalculateTransformation()
        {

            //Поворачиваем вокруг оси z,

            //чтобы точка наблюдения оказалась в плоскости y-z:

            Matrix3d transformation1 = Matrix3d.GetZRotPointToYZ(_eye);

            //Поворачиваем вокруг оси x,

            //чтобы точка наблюдения оказалась на оси z:

            Matrix3d transformation2 = Matrix3d.GetXRotPointToZ(_eye);

            //Проецируем по оси z, игнорируя координату z.

            //Для этого умножаем матрицы преобразования:

            return (transformation1.MultiplyR(transformation2));

        }

        public void Plot(Plot3d plot3d)
        {
            if (_graphics == null) {
                _graphics = plotBox.CreateGraphics();
            }

            _graphics.Clear(Color.White);
            _graphics.SmoothingMode = SmoothingMode.AntiAlias;

            _graphics.ScaleTransform(plotBox.Width / _scalW,
                -plotBox.Height / _scalH, MatrixOrder.Append);
            
            _graphics.TranslateTransform(plotBox.Width / _tranW,
                plotBox.Height / _tranH, MatrixOrder.Append);
            
            var transformMatrix = CalculateTransformation();
            var points = new Point3d[_xLen, _yLen];

            double ZMin, ZMax;
            PrepareData(points, _xLen, _yLen, plot3d, transformMatrix, out ZMin, out ZMax);

            _axes[0] = new Point3d(plot3d.XMin, plot3d.YMin, ZMin);
            _axes[1] = new Point3d(plot3d.XMax, plot3d.YMin, ZMin);
            _axes[2] = new Point3d(plot3d.XMin, plot3d.YMax, ZMin);
            _axes[3] = new Point3d(plot3d.XMin, plot3d.YMin, ZMax);
            DrawAxis(transformMatrix, _addingLines, _addingLines, _addingLines);

            
            PlotData(points, _xLen, _yLen);

            _graphics = null;
            _plot3D = plot3d;
        }

        private void DrawAxis(Matrix3d transformatedMatrix, int xLines, int yLines, int zLines)
        {
            for (int k = 0; k < 4; ++k)
            {
                _axes[k].Transformation(transformatedMatrix);
            }

            Pen penAxes = new Pen(Color.Red, 0);

            double x1 = _axes[0].TransCoord[0];
            double y1 = _axes[0].TransCoord[1];

            //x-axe
            double x2 = _axes[1].TransCoord[0];
            double y2 = _axes[1].TransCoord[1];

            //y-axe
            double x3 = _axes[2].TransCoord[0];
            double y3 = _axes[2].TransCoord[1];

            //z-axe
            double x4 = _axes[3].TransCoord[0];
            double y4 = _axes[3].TransCoord[1];

            //additional point
            Point3d pTemp = new Point3d(_axes[1].OrigCoord[0], _axes[2].OrigCoord[1], _axes[0].OrigCoord[2]);
            pTemp.Transformation(transformatedMatrix);
            double x5 = pTemp.TransCoord[0];
            double y5 = pTemp.TransCoord[1];

            _graphics.DrawLine(penAxes, (float)x1, (float)y1, (float)x2, (float)y2);
            _graphics.DrawLine(penAxes, (float)x1, (float)y1, (float)x3, (float)y3);
            _graphics.DrawLine(penAxes, (float)x1, (float)y1, (float)x4, (float)y4);

            double yOff = 1;

            double xTemp1, yTemp1, xTemp2, yTemp2;

            for (int i = 1; i <= xLines; ++i )
            {
                xTemp1 = (x1 + yOff / xLines * i * (x2 - x1));
                yTemp1 = (y1 + yOff / xLines * i * (y2 - y1));
                xTemp2 = (x3 + yOff / xLines * i * (x5 - x3));
                yTemp2 = (y3 + yOff / xLines * i * (y5 - y3));
                _graphics.DrawLine(penAxes, (float)xTemp1, (float)yTemp1, (float)xTemp2, (float)yTemp2);
            }

            for (int i = 1; i <= yLines; ++i)
            {
                xTemp1 = (x1 + yOff / yLines * i * (x3 - x1));
                yTemp1 = (y1 + yOff / yLines * i * (y3 - y1));
                xTemp2 = (x2 + yOff / yLines * i * (x5 - x2));
                yTemp2 = (y2 + yOff / yLines * i * (y5 - y2));
                _graphics.DrawLine(penAxes, (float)xTemp1, (float)yTemp1, (float)xTemp2, (float)yTemp2);
            }

            pTemp = new Point3d(_axes[1].OrigCoord[0], _axes[0].OrigCoord[1], _axes[3].OrigCoord[2]);
            pTemp.Transformation(transformatedMatrix);
            x5 = pTemp.TransCoord[0];
            y5 = pTemp.TransCoord[1];
            _graphics.DrawLine(penAxes, (float)x2, (float)y2, (float)x5, (float)y5);

            for (int i = 1; i <= zLines; ++i)
            {
                xTemp1 = (x1 + yOff / zLines * i * (x4 - x1));
                yTemp1 = (y1 + yOff / zLines * i * (y4 - y1));
                xTemp2 = (x2 + yOff / zLines * i * (x5 - x2));
                yTemp2 = (y2 + yOff / zLines * i * (y5 - y2));
                _graphics.DrawLine(penAxes, (float)xTemp1, (float)yTemp1, (float)xTemp2, (float)yTemp2);
            }

            pTemp = new Point3d(_axes[0].OrigCoord[0], _axes[2].OrigCoord[1], _axes[3].OrigCoord[2]);
            pTemp.Transformation(transformatedMatrix);
            x5 = pTemp.TransCoord[0];
            y5 = pTemp.TransCoord[1];
            _graphics.DrawLine(penAxes, (float)x3, (float)y3, (float)x5, (float)y5);

            for (int i = 1; i <= zLines; ++i)
            {
                xTemp1 = (x1 + yOff / zLines * i * (x4 - x1));
                yTemp1 = (y1 + yOff / zLines * i * (y4 - y1));
                xTemp2 = (x3 + yOff / zLines * i * (x5 - x3));
                yTemp2 = (y3 + yOff / zLines * i * (y5 - y3));
                _graphics.DrawLine(penAxes, (float)xTemp1, (float)yTemp1, (float)xTemp2, (float)yTemp2);
            }
        }

        private void PlotBoxPaint(object sender, PaintEventArgs e)
        {
            if (_plot3D != null)
            {
                _graphics = e.Graphics;
                Plot(_plot3D);
            }
        }

        protected override bool ProcessCmdKey( ref Message msg,
            Keys keyData)
        {
            const double deltaTheta = Math.PI / 32;

            const double deltaPhi = Math.PI / 16;

            double theta = Math.Atan2(_eye.OrigCoord[1], _eye.OrigCoord[0]);

            double r1 = Math.Sqrt(_eye.OrigCoord[0] * _eye.OrigCoord[0] + _eye.OrigCoord[1] 
                * _eye.OrigCoord[1]);

            double r2 = Math.Sqrt(_eye.OrigCoord[0] * _eye.OrigCoord[0] + _eye.OrigCoord[1]
                * _eye.OrigCoord[1] + _eye.OrigCoord[2] * _eye.OrigCoord[2]);

            double phi = Math.Atan2(_eye.OrigCoord[2], r1);

            switch (keyData)
            {
                case Keys.Left:

                    theta = theta - deltaTheta;

                    break;

                case Keys.Up:

                    phi = phi - deltaPhi;

                    if (phi < -Math.PI / 2) phi = -Math.PI / 2;

                    break;

                case Keys.Right:

                    theta = theta + deltaTheta;

                    break;

                case Keys.Down:

                    phi = phi + deltaPhi;

                    if (phi > Math.PI / 2) phi = Math.PI / 2;

                    break;

                case Keys.Add:
                    {
                        const float step = 1;
                        _scalW += step;
                        _scalH += step;
                    }
                    break;

                case Keys.Subtract:
                    {
                        const float step = 1;
                        _scalW -= step;
                        _scalH -= step;
                    }
                    break;

                case Keys.Q:
                    {
                        const int add = 1;
                        _xLen += add;
                    }
                    break;

                case Keys.A:
                    {
                        const int add = 1;
                        _xLen -= add;
                    }
                    break;

                case Keys.W:
                    {
                        const int add = 1;
                        _yLen += add;
                    }
                    break;

                case Keys.S:
                    {
                        const int add = 1;
                        _yLen -= add;
                    }
                    break;

                case Keys.E:
                    {
                        const int add = 1;
                        _addingLines += add;
                    }
                    break;

                case Keys.D:
                    {
                        const int add = 1;
                        _addingLines -= add;
                    }
                    break;
            }

            _eye.OrigCoord[0] = r1 * Math.Cos(theta);
            _eye.OrigCoord[1] = r1 * Math.Sin(theta);
            _eye.OrigCoord[2] = r2 * Math.Sin(phi);

            plotBox.Invalidate();

            return true;

        }
    }

    public class Plot3d
    {
        public Func<double, double, double> Z { get; set; }
        public double XMin { get; set; }
        public double XMax { get; set; }
        public double YMin { get; set; }
        public double YMax { get; set; }
    }


    public class Point3d
    {
        public double[] OrigCoord = new double[3];
        public double[] TransCoord = new double[3];

        public Point3d(double x, double y, double z)
        {
            OrigCoord[0] = x;
            OrigCoord[1] = y;
            OrigCoord[2] = z;
        }

        public void Transformation(Matrix3d matrix)
        {
            double value = 0;

            for (int i = 0; i < 3; ++i) 
            {
                value = 0;

                for (int j = 0; j < 3; ++j) 
                {
                    value += OrigCoord[j] * matrix.M[j, i];
                }

                TransCoord[i] = value;
            }
        }

    }

    public class Matrix3d
    {
        public double[,] M = new double[3, 3];

        public Matrix3d() {}

        public Matrix3d( double m00, double m01, double m02,
                         double m10, double m11, double m12,
                         double m20, double m21, double m22)
        {

            M[0, 0] = m00; M[0, 1] = m01; M[0, 2] = m02;

            M[1, 0] = m10; M[1, 1] = m11; M[1, 2] = m12;

            M[2, 0] = m20; M[2, 1] = m21; M[2, 2] = m22;

        }

        public Matrix3d MultiplyR(Matrix3d rightMatrix)
        {
            var result = new Matrix3d();

            for (int i = 0; i < 3; ++i)
            {
                for (int j = 0; j < 3; ++j)
                {
                    double value = 0;

                    for (int k = 0; k < 3; ++k)
                    {
                        value += M[i, k] * rightMatrix.M[k, j];
                    }

                    result.M[i, j] = value;
                }
            }

            return result;

        }

        //Метод для поворота вокруг оси z

        //до точки в y-z плоскости:

        public static Matrix3d GetZRotPointToYZ(Point3d point3d)
        {

            double len = Math.Sqrt( point3d.OrigCoord[0] * point3d.OrigCoord[0] +
                                  point3d.OrigCoord[1] * point3d.OrigCoord[1]);

            double stheta = point3d.OrigCoord[0] / len;

            double ctheta = point3d.OrigCoord[1] / len;

            return new Matrix3d( ctheta,   stheta, 0,
                                 -stheta,  ctheta, 0,
                                 0,        0,      1);
        }

        //Метод для поворота вокруг оси x до точки на оси z:

        public static Matrix3d GetXRotPointToZ(Point3d point3d)
        {
            double lenXYZ = Math.Sqrt( point3d.OrigCoord[0] * point3d.OrigCoord[0] +
                                       point3d.OrigCoord[1] * point3d.OrigCoord[1] +
                                       point3d.OrigCoord[2] * point3d.OrigCoord[2]);

            double cphi = -point3d.OrigCoord[2] / lenXYZ;
            double sphi = Math.Sqrt(1 - cphi*cphi);

            return new Matrix3d( 1, 0,    0,
                                 0, cphi, 1,
                                 0, 1,    cphi);
        }
    }
}
